package edu.mercer.sse554.bkp;

import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.base.Stopwatch;
import com.google.common.base.Throwables;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

/**
 * Run compression for a path argument provided. Accept configuration 
 * 	arguments also.<p>
 *
 */

public class CompressApp implements ConfigurationConstants
{	
	private final static ExecutorService _excPool = Executors
			.newCachedThreadPool();
	
	private final static ListeningExecutorService _threadExe = MoreExecutors
			.listeningDecorator(_excPool);

	private static Boolean _verbose = false;
	
	
	public static void main( final String[] args ) 
	{		
		try
		{
			if( args.length < 1 ) halt( HELP );
			
			_verbose = Arrays.asList(args)
							 .contains("--verbose");
			
			/* 
			 * Command Pattern 'Invoker' : inline, runs once, no meaningful 
			 * 	undo
			 * 
			 */
			
			final List<Path> filesFound = GetFilesCommand.factory(args)
														 .execute();

			List<SnappyCompression> compressionTasks = Lists.newArrayList(); 
					
			Iterator<SnappyCompression> jobs =	
						Lists.transform( 
										filesFound,
										CompressionTaskCreation.factory() )
					.iterator();
			
			/* 
			 * Collections.copy method does not work with Guava view of 
			 * transform method 
			 */
			
			while( jobs.hasNext() ) compressionTasks.add( jobs.next() );
				
			
			
//			Collections.copy( compressionTasks,	Lists.transform(
//					
//								filesFound,
//								
//							    CompressionTaskCreation.factory() ) );
			
			/*
			 * Command pattern:
			 * 
			 * Mask submit, pass listening executor service reference.
			 * 
			 * Ordered List of returned ListenableFutures for each submit() 
			 * 	call.
			 * 
			 * Each call to command.perform(task, threadExe) adds a
			 * 	ListenableFuture to the command's history list
			 * 
			 * Command encapsulates/exposes each task async progress 
			 * 	percentage
			 * 
			 * Command encapsulates/exposes each task abend boolean
			 * 
			 * ----------------------------------------------------------
			 * As commands finish,
			 *	...iterating through them, checkedGet is called from
			 *
			 * 2nd Command Example:
			 * 	Command2.evaluate() expects BackupException from the 
			 * 	checkedGet, uses logging visitor, does not convert or throw
			 * 	depending on arg-option set
			 * 
			 * performs Command2.undo to remove any uncompleted or 
			 * 	half-written compressed file
			 * 
			 * -----------------------------------------------------------
			 * 
			 * 3rd Command example:
			 * 
			 * Could have macro command/composite or collection of the
			 * 	individual command instances
			 * 
			 * Provides an overall taskbar
			 * 
			 * Provides monitoring, timed execution of all tasks, selectively
			 * 	showing individual slow/large file progress bars
			 * 
			 * -----------------------------------------------------------
			 * 
			 * Could simply mask complexity of what is in main() currently
			 * 	with a command(s) interfaces/patterns
			 */
			
			checkArgument( compressionTasks.size() > 0, "nothing to do" );
			
			/* 
			 * invokeAll awaits completion, use submit on each instead 
			 */
			
			Iterator<SnappyCompression> taskIter = compressionTasks.iterator();
			
			while( taskIter.hasNext() )_threadExe.submit( taskIter.next() );

			/* 
			 * guava predicate for filter of total number of completed tasks
			 * 
			 * use to show % remaining / progress
			 */
			
			Float completedTasks = new Float( Collections2
								.filter(compressionTasks, 
										Completed.factory() )
								.size() );
			
			Float prevTally = 0F;
			
			Integer percentCompleted = 0;

			Integer prevPercent = 0;

			Stopwatch timer = Stopwatch.createStarted();
			
			while( completedTasks < new Float(compressionTasks.size()) )
			{
				prevTally = completedTasks;
				
				completedTasks = new Float( Collections2
						.filter(compressionTasks, Completed.factory() )
						.size() );
				
				if( prevTally != completedTasks )
				{
					prevPercent = percentCompleted;
					
					percentCompleted = Math.round( ( completedTasks / 
							compressionTasks.size() ) 
													 * 100.0F );

					if( prevPercent != percentCompleted )
					{
						for( Integer ct=0; 
							 ct <= new Integer(percentCompleted/10); 
							 ct++ ) System.out.print( ".");

						System.out.printf( "%d%%%n", percentCompleted );
						
					}//if
				
					/*
					 * TODO
					 * 
					 * guava stopwatch, show file detail when slow progress 
					 * 
					 * show individual task % being worked on, use another 
					 * loop
					 */
				}
				else if( timer.elapsed(TimeUnit.SECONDS) > 
						 Integer.valueOf( Settings
								 		   .access()
								 		   .getProperty("STARTUP-TIMEOUT")) )
				{
					throw new BackupUncheckedException(	"Possible looping," +
							" no progress after several seconds");
					
				}//if
				
			}//while
			
			
			for( SnappyCompression taskCurrent : compressionTasks )
			{
				/* 
				 * BackupException checked exceptions are thrown here, as 
				 * checkedGet is finally called AFTER all tasks are finished
				 */
				
				taskCurrent.checkedGet();
				
				if( _verbose )
				{
					SnappyCRBuilder.factory()
								   .setTask( taskCurrent )
								   .build()
								   .print();
					
				}//if
				
			}//for
							
		}
		catch( final BackupException err )
		{
			/* 
			 * Visitor: creates new object from state of object implementing 
			 * 	accept method
			 * 
			 * Using to convert one type of checked exception to a type of 
			 * 	unchecked
			 * 
			 * Visitor type describes action instead of method name
			 */
			
			err.accept( BackupErrorLoggingVisitor.factory() );
			
			throw err.accept( BackupErrorConversionVisitor.factory() );
			
		}
		catch( final BackupSettingsException err)
		{
			err.accept( BackupErrorLoggingVisitor.factory() );			
			
			throw err;
			
		}
		catch( final BackupUncheckedException err)
		{
			err.accept( BackupErrorLoggingVisitor.factory() );			
			
			throw err;
			
		}catch( final Throwable err ){
			
			final BackupThrowable errExposed = new BackupThrowable( err );
			
			errExposed.accept( BackupErrorLoggingVisitor.factory() );
			
			Throwables.propagate( errExposed );
			
		}
		finally
		{
			halt();

		}//try
		
	}//method


	public static void halt()
	{
		halt( 0, "" );
		
	}//method
	

	public static void halt( final String msg )
	{
		halt( 0, msg );
		
	}//method

	
	public static void halt( final Integer returnCode, final String msg )
	{
		if( msg.length() > 0 ) System.out.println( msg );
		
		SnappyCompression.shutdownAnExecutor( _threadExe );
		
		System.exit( returnCode );
		
	}//method

	
}//class



/**
 * Guava Predicate: Remove program argument options.<p>
 * 
 * Removal filter for non-paths from command line arguments.<p>
 *
 */

class OptionsRemoved implements Predicate<String>
{

	@Override
	public boolean apply( final String recIn ) 
	{
		return ! recIn.equals( "--verbose" );
		
	}//method
	

}//class


/**
 * Recursive list of files for a Path(s).<p>
 * 
 * Command Pattern 'Receiver'<p>
 * 
 * Program arguments include Path(s). Recursive list of files as Path 
 * objects.<p>
 *
 * Required by / Parameter object for GetFilesCommand.<p>  
 *
 */

class FilesSpecified 
{
	final Set<Path> _locations = new HashSet<>();
	
	
	private FilesSpecified()
	{
		super();
		
	}//constructor
	
	
	public static FilesSpecified factory()
	{
		return new FilesSpecified();
		
	}//method


	public FilesSpecified withPath( final String pathIn )
	{
		final Path[] pathsArr = { Paths.get( pathIn ) };
		
		return withPathArray( pathsArr );
		
	}//method

	
	public FilesSpecified withPath( final Path pathIn )
	{
		final Path[] pathsArr = {pathIn};
		
		return withPathArray( pathsArr );
		
	}//method

	
	public FilesSpecified withPathArray( final String[] pathArrIn )
	{
		final Path[] pathsArr = new Path[pathArrIn.length];
		
		for( Integer ct=0; ct < pathArrIn.length; ct++ )
		{ 
			pathsArr[ct] = Paths.get( pathArrIn[ct] );
			
		}//for
		
		return withPathArray( pathsArr );
		
	}//method
	
	
	public FilesSpecified withPathArray( final Path[] pathArrIn )
	{
		for( Integer ct=0; ct < pathArrIn.length; ct++ )
		{
			_locations.add( pathArrIn[ct] );
			
		}//for
		
		return this;
		
	}//method
	
	
	public FilesSpecified withPathCollection( 
			final Collection<Path> pathsIn )
	{
		_locations.addAll( pathsIn );
		
		return this;
		
	}//method

	
	public FilesSpecified withStringCollection(
			final Collection<String> pathsIn )
	{
		for( String pathStr : pathsIn )
		{
			_locations.add( Paths.get( pathStr ) );
		
		}//for
		
		return this;
		
	}//method
	
	
	public FilesSpecified useArgs( final String[] args )
	{
		withStringCollection( Collections2.filter( Arrays.asList(args), 
												   new OptionsRemoved() ) );		
		
		return this;
		
	}//method
	
	
	public ImmutableList<Path> list() throws IOException 
	{
		final List<Path> pathsFound = new ArrayList<>();
		
		final Iterator<Path> pathIterator = _locations.iterator();
		
		while( pathIterator.hasNext() )
		{
			Path pathCurrent = pathIterator.next();
			
			if( pathCurrent
					 .toFile()
					 .isDirectory() )
			{
				pathsFound.addAll( FolderContents.factory( pathCurrent )
												 .fileSet() );
			}
			else
			{
				if( pathCurrent != null ) pathsFound.add( pathCurrent );
				
			}//if
			
		}//for
		
		if( pathsFound.size() < 1 ) throw new BackupUncheckedException(
				"No files found");
		
		return ImmutableList.copyOf( pathsFound );
		
	}//method

	
	@Fulfills("Object")
	@Override
	public String toString()
	{
		return _locations.toString();
		
	}//method	

	
}//class


/**
 * Retrieve list of files specified.<p>
 * 
 * Command Pattern 'Concrete Command'<p>
 *
 */

class GetFilesCommand implements Command<ImmutableList<Path>>
{
	/* FilesSpecified: parameter object instance */

	private Optional<FilesSpecified> _filesSpecified; 

	
	private GetFilesCommand()
	{
		super();
		
	}//constructor
	
	
	public static GetFilesCommand factory()
	{
		return new GetFilesCommand();
		
	}//method
	
	
	public static GetFilesCommand factory( final String[] args )
	{
		return new GetFilesCommand().withFiles(  FilesSpecified
													.factory()
													.useArgs(args) );
		
	}//method
	
	
	public GetFilesCommand withFiles( 
			final FilesSpecified filesSpecified )
	{
		_filesSpecified = Optional.of( filesSpecified );
		
		return this;
		
	}//method
	

	/**
	 * Execute command: Command Pattern.<p>
	 * 
	 * Run the FilesSpecified list() method. Hides complexity, complies with 
	 * 	Command Interface: suppresses and logs IOexception (using visitor).
	 * 	<p>
	 */
	
	@Override
	public ImmutableList<Path> execute() 
	{
		checkArgument( _filesSpecified.isPresent(), 
				"Initialization required: first use withFiles method" );
		
		List<Path> filePaths = Collections.emptyList();   
		
		try
		{
			filePaths = _filesSpecified.get().list();
			
		}
		catch( final IOException err )
		{
			new BackupException( err )
				.accept( BackupErrorLoggingVisitor.factory() );
			
		}//try
		
		return ImmutableList.copyOf( filePaths );
		
	}//method
	
	
	@Fulfills("Object")
	@Override
	public String toString()
	{
		return _filesSpecified.toString();
		
	}//method
	
	
}//class


